package gr.uoa.di.thanos.botcraft;

import java.awt.DisplayMode;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

import org.lwjgl.LWJGLException;
import org.lwjgl.opencl.CLDevice;
import org.lwjgl.opencl.CLPlatform;

import static org.lwjgl.opencl.CL.*;
import static org.lwjgl.opencl.CL10.*;


/**
 * This class represents a configuration.
 * @author thanos.papapetrou@gmail.com
 *
 */
public class Configuration {
	public static final int DEFAULT_FRAME_RATE = 30;
	private static final String RESOURCE_BUNDLE = "i18n/botcraft";
	private static final String LOCALE = "locale";
	private static final String SCREEN = "screen";
	private static final String DISPLAY_MODE_WIDTH = "displayModeWidth";
	private static final String DISPLAY_MODE_HEIGHT = "displayModeHeight";
	private static final String DISPLAY_MODE_BIT_DEPTH = "displayModeBitDepth";
	private static final String DISPLAY_MODE_REFRESH_RATE = "displayModeRefreshRate";
	private static final String FRAME_RATE = "frameRate";
	private static final String DISPLAY_FULL_SCREEN = "displayFullScreen";
	private static final String OPEN_CL_PLATFORM = "openCLPlatform";
	private static final String OPEN_CL_DEVICE = "openCLDevice";
	private static final String SHARE_GL_CL_CONTEXT = "shareGLCLContext";
	private static final Logger LOGGER = Logger.getLogger(Configuration.class.getName());

	private final Preferences preferences;
	private Locale locale;
	private GraphicsDevice screen;
	private DisplayMode displayMode;
	private int frameRate;
	private boolean displayFullScreen;
	private CLPlatform clPlatform;
	private CLDevice clDevice;
	private boolean shareGlClContext;
	
	/**
	 * Construct a new configuration.
	 */
	public Configuration() {
		preferences = Preferences.userNodeForPackage(Configuration.class);
		locale = null;
		final Locale[] locales = getLocales();
		for (Locale locale : locales) {
			if (locale.getLanguage().equals(preferences.get(LOCALE, null)))
				this.locale = locale;
		}
		if (locale == null)
			locale = Arrays.asList(locales).contains(Locale.getDefault()) ? Locale.getDefault() : ((locales.length > 0) ? locales[0] : null);
		Locale.setDefault(locale);
		screen = null;
		for (GraphicsDevice screen : getScreens()) {
			if (screen.getIDstring().equals(preferences.get(SCREEN, null)))
				this.screen = screen;
		}
		if (screen == null)
			screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
		displayMode = null;
		for (DisplayMode displayMode : getDisplayModes(screen)) {
			if ((displayMode.getWidth() == preferences.getInt(DISPLAY_MODE_WIDTH, 0)) && (displayMode.getHeight() == preferences.getInt(DISPLAY_MODE_HEIGHT, 0)) &&
					(displayMode.getBitDepth() == preferences.getInt(DISPLAY_MODE_BIT_DEPTH, 0)) && (displayMode.getRefreshRate() == preferences.getInt(DISPLAY_MODE_REFRESH_RATE, 0)))
				this.displayMode = displayMode;
		}
		if (displayMode == null)
			displayMode = getScreen().getDisplayMode();
		frameRate = preferences.getInt(FRAME_RATE, DEFAULT_FRAME_RATE);
		displayFullScreen = preferences.getBoolean(DISPLAY_FULL_SCREEN, false);
		clPlatform = null;
		final CLPlatform[] clPlatforms = getOpenCLPlatforms();
		for (CLPlatform clPlatform : clPlatforms) {
			if (clPlatform.getPointer() == preferences.getLong(OPEN_CL_PLATFORM, 0l))
				this.clPlatform = clPlatform;
		}
		if (clPlatform == null)
			clPlatform = (clPlatforms.length > 0) ? clPlatforms[0] : null;
		clDevice = null;
		final CLDevice[] clDevices = getOpenCLDevices(clPlatform);
		for (CLDevice clDevice : clDevices) {
			if (clDevice.getPointer() == preferences.getLong(OPEN_CL_DEVICE, 0l))
				this.clDevice = clDevice;
		}
		if (clDevice == null)
			clDevice = (clDevices.length > 0) ? clDevices[0] : null;
		shareGlClContext = preferences.getBoolean(SHARE_GL_CL_CONTEXT, true);
	}
	
	/**
	 * Get all the available locales.
	 * @return the available locales
	 */
	public Locale[] getLocales() {
		final Set<Locale> locales = new HashSet<Locale>();
		for (Locale locale : Locale.getAvailableLocales()) {
			if (ResourceBundle.getBundle(RESOURCE_BUNDLE, locale).getLocale().equals(locale))
				locales.add(locale);
		}
		return locales.toArray(new Locale[0]);
	}
	
	/**
	 * Get the currently selected locale.
	 * @return the currently selected locale
	 */
	public Locale getLocale() {
		return locale;
	}
	
	/**
	 * Get the resource bundle of the currently selected locale.
	 * @return the resource bundle of the currently selected locale
	 */
	public ResourceBundle getResourceBundle() {
		return ResourceBundle.getBundle(RESOURCE_BUNDLE, locale);
	}
	
	/**
	 * Set the currently selected locale and save this configuration.
	 * @param locale the locale to set
	 * @throws BackingStoreException if any errors occur while saving configuration
	 */
	public void setLocale(final Locale locale) throws BackingStoreException {
		preferences.put(LOCALE, locale.getLanguage());
		preferences.flush();
		this.locale = locale;
	}
	
	/**
	 * Get all the available screens. 
	 * @return the available screens
	 */
	public GraphicsDevice[] getScreens() {
		return GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
	}
	
	/**
	 * Get the currently selected screen. 
	 * @return the currently selected screen
	 */
	public GraphicsDevice getScreen() {
		return screen;
	}
	
	/**
	 * Set the currently selected screen and save this configuration.
	 * @param screen the screen to set
	 * @throws BackingStoreException if any errors occur while saving configuration
	 */
	public void setScreen(final GraphicsDevice screen) throws BackingStoreException {
		preferences.put(SCREEN, screen.getIDstring());
		preferences.flush();
		this.screen = screen;
	}
	
	/**
	 * Get all the available display modes for a screen.
	 * @param screen the screen whose available display modes to get
	 * @return the available display modes of the screen specified
	 */
	public DisplayMode[] getDisplayModes(final GraphicsDevice screen) {
		return screen.getDisplayModes();
	}

	/**
	 * Get the currently selected display mode.
	 * @return the currently selected display mode
	 */
	public DisplayMode getDisplayMode() {
		return displayMode;
	}
	
	/**
	 * Set the currently selected display mode and save this configuration.
	 * @param displayMode the display mode to set
	 * @throws BackingStoreException if any errors occur while saving configuration
	 */
	public void setDisplayMode(final DisplayMode displayMode) throws BackingStoreException {
		preferences.putInt(DISPLAY_MODE_WIDTH, displayMode.getWidth());
		preferences.putInt(DISPLAY_MODE_HEIGHT, displayMode.getHeight());
		preferences.putInt(DISPLAY_MODE_BIT_DEPTH, displayMode.getBitDepth());
		preferences.putInt(DISPLAY_MODE_REFRESH_RATE, displayMode.getRefreshRate());
		preferences.flush();
		this.displayMode = displayMode;
	}
	
	/**
	 * Check if display full screen is currently enabled.
	 * @return true if display full screen is enabled, false otherwise
	 */
	public boolean isDisplayFullScreen() {
		return displayFullScreen;
	}
	
	/**
	 * Set the display full screen status and save this configuration.
	 * @param displayFullScreen the display full screen status to set
	 * @throws BackingStoreException if any errors occur while saving configuration
	 */
	public void setDisplayFullScreen(final boolean displayFullScreen) throws BackingStoreException {
		preferences.putBoolean(DISPLAY_FULL_SCREEN, displayFullScreen);
		preferences.flush();
		this.displayFullScreen = displayFullScreen;
	}
	
	/**
	 * Get the currently selected frame rate.
	 * @return the currently selected frame rate
	 */
	public int getFrameRate() {
		return frameRate;
	}
	
	/**
	 * Set the frame rate and save this configuration.
	 * @param frameRate the frame rate to set
	 * @throws BackingStoreException if any errors occur while saving configuration
	 */
	public void setFrameRate(final int frameRate) throws BackingStoreException {
		preferences.putInt(FRAME_RATE, frameRate);
		preferences.flush();
		this.frameRate = frameRate;
	}

	/**
	 * Get all the available OpenCL platforms.
	 * @return the available OpenCL platforms
	 */
	public CLPlatform[] getOpenCLPlatforms() {
		try {
			create();
			final CLPlatform[] clPlatforms = CLPlatform.getPlatforms().toArray(new CLPlatform[0]); 
			destroy();
			return clPlatforms;
		} catch (final LWJGLException e) {
			LOGGER.log(Level.WARNING, "Error retrieving OpenCL platforms", e);
			return new CLPlatform[0];
		}
		
	}
	
	/**
	 * Get the currently selected OpenCL platform. 
	 * @return the currently selected OpenCL platform
	 */
	public CLPlatform getOpenCLPlatform() {
		return clPlatform;
	}
	
	/**
	 * Set the currently selected OpenCL platform and save this configuration.
	 * @param clPlatform the OpenCL platform to set
	 * @throws BackingStoreException if any errors occur while saving configuration
	 */
	public void setOpenCLPlatform(final CLPlatform clPlatform) throws BackingStoreException {
		preferences.putLong(OPEN_CL_PLATFORM, clPlatform.getPointer());
		preferences.flush();
		this.clPlatform = clPlatform;
	}
	
	/**
	 * Get all the available OpenCL devices for an OpenCL platform.
	 * @param clPlatform the OpenCL platform whose available OpenCL devices to get
	 * @return the available OpenCL devices of the OpenCL platform specified
	 */
	public CLDevice[] getOpenCLDevices(final CLPlatform clPlatform) {
		return clPlatform.getDevices(CL_DEVICE_TYPE_ALL).toArray(new CLDevice[0]);
	}

	/**
	 * Get the currently selected OpenCL device.
	 * @return the currently selected OpenCL device
	 */
	public CLDevice getOpenCLDevice() {
		return clDevice;
	}
	
	/**
	 * Set the currently selected OpenCL device and save this configuration.
	 * @param clDevice the OpenCL device to set
	 * @throws BackingStoreException if any errors occur while saving configuration
	 */
	public void setOpenCLDevice(final CLDevice clDevice) throws BackingStoreException {
		preferences.putLong(OPEN_CL_DEVICE, clDevice.getPointer());
		preferences.flush();
		this.clDevice = clDevice;
	}
	
	/**
	 * Check if sharing context between OpenGL and OpenCL is currently enabled.
	 * @return true if sharing context between OpenGL and OpenCL is enabled, false otherwise
	 */
	public boolean isShareGlClContext() {
		return shareGlClContext;
	}
	
	/**
	 * Set the sharing context status between OpenGL and OpenCL and save this configuration.
	 * @param shareGlClContext the sharing context status between OpenGL and OpenCL to set
	 * @throws BackingStoreException if any errors occur while saving configuration
	 */
	public void setShareGlClContext(final boolean shareGlClContext) throws BackingStoreException {
		preferences.putBoolean(SHARE_GL_CL_CONTEXT, shareGlClContext);
		preferences.flush();
		this.shareGlClContext = shareGlClContext;
	}
}
